/*
 * Copyright 2009 Jamie Gennis
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.code.gwt.remoteaction.testing;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import junit.framework.TestCase;
import org.easymock.classextension.EasyMock;

/**
 * The base class for test cases that use mock objects.
 *
 * @author jgennis
 */
public abstract class MockedTestCase extends TestCase {

  private final Set<Field> mockedFields = new HashSet<Field>();

  private boolean needsVerification;

  private final Set<Object> partialMocks = new HashSet<Object>();

  public MockedTestCase(String name) {
    super(name);
    initMockedFields();
  }

  @Override
  protected void setUp() throws Exception {
    super.setUp();
    needsVerification = false;
    createAll();
  }

  @Override
  protected void tearDown() throws Exception {
    if (needsVerification)
      verifyAll();
    destroyAll();
    super.tearDown();
  }

  /**
   * Create a mock of a given class where all methods are mocked except for
   * the specified exceptions.  The exceptions are specified using only the
   * method name.
   *
   * @param <T> the type of the object to mock
   * @param clazz the class of the object to mock
   * @param exceptDeclaredMethods the method names that are NOT to be mocked
   * @return a new mock object of type {@code T}
   */
  protected <T> T createPartialMock(Class<T> clazz, String... exceptDeclaredMethods) {
    if (needsVerification)
      throw new IllegalStateException("createPartialMock was called after replayAll");
    Set<String> exceptedMethods = new HashSet<String>(Arrays.asList(exceptDeclaredMethods));
    Set<Method> mockedMethods = new HashSet<Method>();
    for (Method method : clazz.getDeclaredMethods()) {
      if (!exceptedMethods.contains(method.getName()))
        mockedMethods.add(method);
    }
    T partialMock = EasyMock.createMock(clazz, mockedMethods.toArray(new Method[0]));
    partialMocks.add(partialMock);
    return partialMock;
  }

  /**
   * Set a field of an object with a given name to a particular value.
   * 
   * This works on both private and final fields, and is intended for use with
   * {@link #createPartialMock} for mocking objects with final or private fields.
   * 
   * @param object the object in which to set the field
   * @param fieldName the name of the field to set
   * @param fieldValue the value to which the field is to be set
   * @throws NoSuchFieldException if the field does not exist in the object
   */
  protected void setField(Object object, String fieldName, Object fieldValue)
      throws NoSuchFieldException {
    Class<?> clazz = object.getClass();
    Field field = null;
    do {
      try {
        field = clazz.getDeclaredField(fieldName);
      } catch (NoSuchFieldException ex) {
        clazz = clazz.getSuperclass();
      }
    } while (field == null && clazz != null);
    if (field == null) {
      throw new NoSuchFieldException("The field '" + fieldName + "' was not found in object: "
          + object);
    }
    field.setAccessible(true);
    try {
      field.set(object, fieldValue);
    } catch (IllegalAccessException ex) {
      throw new RuntimeException(ex);
    } finally {
      field.setAccessible(false);
    }
  }

  /**
   * Put all mocked objects into replay mode.
   *
   * @throws IllegalArgumentException
   * @throws IllegalAccessException
   */
  protected void replayAll() throws Exception {
    needsVerification = true;
    for (Field field : mockedFields)
      EasyMock.replay(field.get(this));
    for (Object partialMock : partialMocks)
      EasyMock.replay(partialMock);
  }

  private void createAll() throws Exception {
    for (Field field : mockedFields)
      field.set(this, EasyMock.createMock(field.getType()));
  }

  private void verifyAll() throws Exception {
    for (Field field : mockedFields)
      EasyMock.verify(field.get(this));
    for (Object partialMock : partialMocks)
      EasyMock.verify(partialMock);
  }

  private void destroyAll() throws Exception {
    for (Field field : mockedFields)
      field.set(this, null);
    partialMocks.clear();
  }

  /**
   * Construct the collection of mocked fields for this MockedTestCase
   * instance.
   */
  private void initMockedFields() {
    for (Class<?> cls = getClass(); cls != Object.class; cls = cls.getSuperclass()) {
      for (Field field : cls.getDeclaredFields()) {
        if (field.getAnnotation(Mock.class) != null) {
          mockedFields.add(field);
          field.setAccessible(true);
        }
      }
    }
  }
}
